





function UvmRelations() {
    
    this.uuid                 = null;
    this.modules_hierarchy    = {}; // just a collection of id => id
    this.modules              = {};
    this.modules_seq          = {};
    this.modules_seq_id       = 0;
    this.assemblies_cache     = {}; // references to objects to avoid re-creating
    this.assemblies           = {};
    this.assemblies_relations = {};
    this.assemblies_seq       = {};
    this.assemblies_seq_id    = 0;
    
    /**
     * Creates relation and sets value, it always sets lower ID as LH-side one
     * 
     * @param $assembly1 UvmAssembly
     * 
     * @param $assembly2 UvmAssembly
     * 
     * @param $buildable string S|O|na  (Standard, optional, not available)
     * 
     * @return bool Op status
     */
    this.addRelation = function($assembly1, $assembly2, $buildable) {
        
        if ($assembly1.isValid()!=true || $assembly2.isValid()!=true) {
            return false;
        }
        
        var $allowed = { 'S'  : true, 'na' : true, 'O' : true };
        var $value   = (null!=$allowed[$buildable]) ? $buildable : null; // null => to be inferred to to inherit in matrix
        var $order   = this._order($assembly1, $assembly2);
        var $lh      = $order['lh'];
        var $rh      = $order['rh'];
        
        if (null==$lh || null==$rh) {
            return false; // err
        }
        
        // adding module to hierarchy
        this._addNewModuleToList($lh);
        this._addNewModuleToList($rh);
        
        this.relateModulesInHierarchy(
            $lh.getModuleId(), 
            $rh.getModuleId()
        );
        
        this.relateAssemblies($lh, $rh, $buildable);
        
        // link objects
        this.assemblies_cache[$lh.id] = $lh;
        this.assemblies_cache[$rh.id] = $rh;
        
        // logging order of appearance
        this.logSequenceId('assembly', $lh.id);
        this.logSequenceId('assembly', $rh.id);
        this.logSequenceId('module', $lh.getModuleId());
        this.logSequenceId('module', $rh.getModuleId());
            
    }
    
    /**
     * Common to both assemblies and modules
     * takes respective ID and if it's first-seen it logs it under incremented id
     * Ids are incremented separatelly for modules and assemblies
     *
     * @param $entity string 'assembly'|'module'
     * 
     * @param $id int Numeric ID
     * 
     * @return bool Op status
     */
    this.logSequenceId = function($entity, $id) {
        
        if (!isNaN($id) && ($entity=='assembly' || $entity=='module')) {
            
            if ($entity=='assembly') {
                
                if (null==this.assemblies_seq[$id]) {
                    this.assemblies_seq[$id] = ++this.assemblies_seq_id;
                    return true;
                } else {
                    return false;
                }
                
            } else if ($entity=='module') {
                
                if (null==this.modules_seq[$id]) {
                    this.modules_seq[$id] = ++this.modules_seq_id;
                    return true;
                } else {
                    return false;
                }
            }
            
        } else {
            
            return false;
        }
        
        
        
    }
    
    /**
     * Finds and removes relation of two assemblies (if exists)
     * Also removes parent module from hierarchy if it was last pair involving particular
     * module's children assemblies
     *
     * @param $assembly1 UvmAssembly
     * 
     * @param $assembly2 UvmAssembly
     * 
     * @return bool Op status
     */
    this.removeRelation = function($assembly1, $assembly2) {
        
    }
    
    /**
     * Removes assembly and all relations it's been involved in
     * 
     * @param $assembly2 UvmAssembly
     * 
     * @return bool Op status
     */
    this.removeAssembly = function($assembly) {
        
    }
    
    
    /**
     * Relates two modules in hierarchy
     * 
     * @param $id1 int Module 1
     * 
     * @param $id2 int Module 2
     *
     * @return bool
     */
    this.relateModulesInHierarchy = function($id1, $id2) {
        
        var $id = '';
        for ($id in this.modules) {
            if (null==this.modules_hierarchy[$id]) {
                this.modules_hierarchy[$id] = {};
            }
        }
        
        this.modules_hierarchy[$id2][$id1] = true;
        this.modules_hierarchy[$id1][$id2] = true;
    }
    
    /**
     * Finds and removes relation of two assemblies (if exists)
     * Also removes parent module from hierarchy if it was last pair involving particular
     * module's children assemblies
     *
     * @param $lh UvmAssembly
     * 
     * @param $rh UvmAssembly
     * 
     * @return bool Op status
     */    
    this.relateAssemblies = function($lh, $rh, $value) {
        
        if ($lh.isValid()!=true || $rh.isValid()!=true) {
            return false;
        }
        
        this.assemblies[$lh.id] = true;
        this.assemblies[$rh.id] = true;
        
        var $id = '';
        for ($id in this.assemblies) {
            if (null==this.assemblies_relations[$id]) {
                this.assemblies_relations[$id] = {};
            }
        }        
        
        this.assemblies_relations[$lh.id][$rh.id] = $value;
        this.assemblies_relations[$rh.id][$lh.id] = $value;
    }
    
    /**
     * Gets modules hierarchy with possible 1:1 or 1:n lookups
     * 
     * @param $format string|null Export-to-JSON flag (value: string)
     * 
     * @return object|string
     */    
    this.getModulesHierarchy = function($format) {
        var $new       = {};
        var $lh_module = 0;
        var $rh_module = 0;
        var $sub       = [];
        for ($lh_module in this.modules_hierarchy) {
            for ($rh_module in this.modules_hierarchy[$lh_module]) {
                if (null==$new[$lh_module]) {
                    $new[$lh_module] = [];
                }
                $new[$lh_module].push($rh_module);
            }
        }
        
        if (null==$format) {
            return $new;
        } else {
            
            var $top = [];
            var $arr = [];
            var $sub = [];
            var $id  = 0;
            
            $lh_module = 0;
            $rh_module = 0;
            
            for ($lh_module in $new) {
                $arr = $new[$lh_module];
                $sub = [];
                for ($id in $arr) {
                    $sub.push($arr[$id]);
                }
                $top.push("   '"+$lh_module+"' : ["+$sub.join(",")+"]");
            }
            
            return "{\n"+$top.join(",\n")+"\n}";            
        }
    }
    
    /**
     * Gets assemblies relations with values where applicable
     * 
     * @param $format string|null Export-to-JSON flag (value: string)
     * 
     * @return object|string
     */
    this.getAssembliesRelations = function($format) {
        if (null==$format) {
            return this.assemblies_relations;
        } else {
            
            var $top = [];
            var $arr = [];
            var $sub = [];
            var $id  = 0;
            
            var $lh_assy = 0;
            var $rh_assy = 0;
            
            for ($lh_assy in this.assemblies_relations) {
                $arr = this.assemblies_relations[$lh_assy];
                $sub = [];
                for ($id in $arr) {
                    $sub.push("'"+$id+"': '"+$arr[$id]+"'");
                }
                $top.push("   '"+$lh_assy+"' : {"+$sub.join(",")+"}");
            }
            
            return "{\n"+$top.join(",\n")+"\n}";
            
        }
    }
    
    /**
     * Gets assemblies sequence in order of appearance
     * 
     * @param $format string|null Export-to-JSON flag (value: string)
     * 
     * @return object|string
     */
    this.getAssembliesSequence = function($format) {
        var $assy_id = 0;
        var $new = {};
        for ($assy_id in this.assemblies_seq) {
            $new[this.assemblies_seq[$assy_id]] = $assy_id;
        }
        if (null!=$format && $format=='string') {
            return this._hashToJSON($new);
        } else {
            return $new;
        }
    }
    
    /**
     * Gets modules sequence in order of appearance
     * 
     * @param $format string|null Export-to-JSON flag (value: string)
     * 
     * @return object|string
     */
    this.getModulesSequence = function($format) {
        var $module_id = 0;
        var $new       = {};
        for ($module_id in this.modules_seq) {
            $new[this.modules_seq[$module_id]] = $module_id;
        }
        if (null!=$format && $format=='string') {
            return this._hashToJSON($new);
        } else {
            return $new;
        }
    }
    
    /**
     * Creates string output for passed hash
     * 
     * @param $hash object Hash to stringify
     *
     * @return string
     */
    this._hashToJSON = function($hash) {
        if ($hash==null) {
            return '{}';
        } else {
            var $exp    = [];
            var $sub    = [];
            var $ord_id = 0;
            $exp.push("{\n");
            for ($ord_id in $hash) {
                $sub.push("   '"+$ord_id+"' : '"+$hash[$ord_id]+"'");
            }
            $exp.push($sub.join(",\n "));
            $exp.push("\n}");
            return $exp.join(' ');        
        }
    }
    
    
    
    /**
     * Removes module and all related assemblies and their respective relations
     * (similar to remove assembly, only may have broader scope)
     * 
     * @param $module UvmModule
     * 
     * @return bool
     */
    this.removeModuleFromHierarchy = function($module) {
        
    }
    
    /**
     * Turns one of lower id into LH side
     * 
     * @param $obj1 UvmAssembly
     * 
     * @param $obj2 UvmAssembly
     * 
     * @return hash
     */
    this._order = function($obj1, $obj2) {
        var $hash = {
            'lh' : null, 'rh' : null
        };
        if ($obj1.isValid()==true && $obj2.isValid()==true) {
            
            var $lh = null;
            var $rh = null;
            
            if (1*$obj1.id<1*$obj2.id) {
                $lh = $obj1;
                $rh = $obj2;
            } else {
                $lh = $obj2;
                $rh = $obj1;
            }
            
            $hash['lh'] = $lh;
            $hash['rh'] = $rh;
        }        
        return $hash;
    }
    
    /**
     * REVIEW!!!
     * Using one assembly at the time it adds assembly's module to modules list
     *
     * @param $obj2 UvmAssembly
     * 
     * @return bool Op status
     */
    this._addNewModuleToList = function($assembly) {
        
        this.modules[$assembly.getModuleId()] = true;
        return (this.modules[$assembly.getModuleId()]==true);
    }
    
    
    /**
     * @param $value string|int Value to find
     * 
     * @param $array Array
     *
     * @return null|bool NULL on err, otherwise Op status
     */
    this._valueExistsInArray = function($value, $array) {
        
        if (null!=$array.length && !isNaN($array.length)) {
            // it's array
            var $i     = 0;
            var $found = false;
            for ($i in $array) {
                if ($array[$i]==$value) {
                    return true;
                }
            }
            return $found;
        } else {
            return null;
        }
    }
    
    
    /**
     * Generates (where applicable) and returns UUID for the whole relations
     *
     * @return string
     */
    this.getUniqueId = function() {
        if (null!=this.uuid) {
            return this.uuid; // always run only once
        }
        var $arr = [];
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        $arr.push(Math.floor(Math.random() * 0x10000).toString(16));
        this.uuid = $arr.join('');
        return this.uuid;
    }
    
    
    
}





function UvmModule() {
    
    
    this.id            = null;
    this.title         = null;
    this.fk_program_id = null;
    this.updated       = null;
    this.created       = null;
    this.comments      = null;
    
    this.db_reg = 'DB';
    this.db = function() {
        return Reg.instance().get(this.db_reg);
    }
    
    this.populateByTitle = function($title) {
        this._populate(this.db().select('module', {'title' : $title}));
        return this.isValid();
    }
    
    this.populateById = function($id) {
        this._populate(this.db().select('module', {'id' : $id}));
        return this.isValid();
    }
    
    this._populate = function($data) {
        if (null==$data) {
            return;
        }
        var $_row = null;
        var $c    = 0;
        var $i    = 0;
        for ($i in $data) {
            
            $_row = $data[$i];
            
            this.id            = (null!=$_row['id']) ? $_row['id'] : null;
            this.title         = (null!=$_row['title']) ? $_row['title'] : null;
            this.fk_program_id = (null!=$_row['fk_program_id']) ? $_row['fk_program_id'] : null;
            this.updated       = (null!=$_row['updated']) ? $_row['updated'] : null;
            this.created       = (null!=$_row['created']) ? $_row['created'] : null;
            this.comments      = (null!=$_row['comments']) ? $_row['comments'] : null;
        }
    }
    
    this.isValid = function() {
        return (this.id!=null 
            && this.title!=null 
            && this.fk_program_id!=null 
            && isNaN(this.id)==false 
            && isNaN(this.title)==true 
            && isNaN(this.fk_program_id)==false
        );
    }
    
    this.getProgramId = function() {
        return this.fk_program_id;
    }    
    
    
}


function UvmAssembly() {
    
    this.id           = null;
    this.title        = null;
    this.fk_module_id = null;
    this.updated      = null;
    this.created      = null;
    this.comments     = null;
    
    this.db_reg = 'DB';
    
    /**
     * @return DbMock
     */
    this.db = function() {
        return Reg.instance().get(this.db_reg);
    }
    
    this.populateByTitle = function($title) {
        this._populate(this.db().select('assembly', {'title' : $title}));
        return this.isValid();
    }
    
    this.populateById = function($id) {
        this._populate(this.db().select('assembly', {'id' : $id}));
        return this.isValid();
    }
    
    this._populate = function($data) {
        if (null==$data) {
            return;
        }
        var $_row = null;
        var $c    = 0;
        var $i    = 0;
        for ($i in $data) {
            
            $_row = $data[$i];
            
            this.id           = (null!=$_row['id']) ? $_row['id'] : null;
            this.title        = (null!=$_row['title']) ? $_row['title'] : null;
            this.fk_module_id = (null!=$_row['fk_module_id']) ? $_row['fk_module_id'] : null;
            this.updated      = (null!=$_row['updated']) ? $_row['updated'] : null;
            this.created      = (null!=$_row['created']) ? $_row['created'] : null;
            this.comments     = (null!=$_row['comments']) ? $_row['comments'] : null;
        }
    }
    
    this.isValid = function() {
        return (this.id!=null 
            && this.title!=null 
            && this.fk_module_id!=null 
            && isNaN(this.id)==false 
            && isNaN(this.title)==true 
            && isNaN(this.fk_module_id)==false
        );
    }
    
    this.getModuleId = function() {
        return this.fk_module_id;
    }
    
    /**
     * Gets parent module object
     * 
     * @return UvmModule
     */
    this.getParentModuleObject = function() {
        if (this.isValid()==true) {
            var $alias = 'module_'+this.getModuleId();
            if (null==Reg.instance().get($alias)) {
                var $obj = new UvmModule();
                $obj.populateById(this.getModuleId());
                Reg.instance().set($alias, $obj, true);
            }
            return Reg.instance().get($alias);
        } else {
            return new UvmModule(); // not a valid instance, but same object
        }
    }
    
}




function DbMock($assy, $mods) {
    
    this.assemblies = $assy; // assemblies storage
    this.modules    = $mods; // modules storage
    
    this.select = function($table, $where)
    {
        var $data = null;
        if ($table=='assembly' || $table=='assemblies') {
            $data = this.assemblies;
        } else if ($table=='module' || $table=='modules') {
            $data = this.modules;
        } else {
            return {}; /// maybe we should throw an exception
        }
        
        var $results = {};
        var $i       = 0;
        var $k       = null;
        var $row     = 0;
        var $m       = -1;
        for ($i in $data) {
            $m = 1; 
            for ($k in $where) {
                if (null==$data[$i][$k] || $data[$i][$k]!=$where[$k]) {
                    $m = 0; // missed filter condition
                }
            }
            if ($m==1) {
                $results[$data[$i]['id']] = $data[$i];
            }
        }
        return $results;
    }
    
}




